package com.gitee.wsl.compose.ui.richtext.edit.state


import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.util.fastForEachIndexed
import com.gitee.wsl.compose.ui.richtext.edit.annotation.ExperimentalRichTextApi
import com.gitee.wsl.compose.ext.text.customMerge
import com.gitee.wsl.compose.ext.text.isSpecifiedFieldsEquals
import com.gitee.wsl.compose.ui.richtext.edit.model.span.RichSpan
import com.gitee.wsl.compose.ui.richtext.edit.model.span.style.RichSpanStyle
import com.gitee.wsl.compose.ext.text.unmerge
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.startText
import com.gitee.wsl.ext.list.removeRange
import kotlin.collections.addAll
import kotlin.math.max


internal fun RichTextState.getRichSpanByOffset(offset: Offset): RichSpan? {
    this.textLayoutResult?.let { textLayoutResult ->
        val position = textLayoutResult.getOffsetForPosition(offset)
        return getRichSpanByTextIndex(position, true)
    }
    return null
}

@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.addRichSpanAtPosition(
    vararg richSpan: RichSpan,
    index: Int,
) {
    val previousIndex = index - 1

    val activeRichSpan = getRichSpanByTextIndex(previousIndex)

    // If there is no active rich span, add the rich span to the last paragraph
    if (activeRichSpan == null) {
        richParagraphList.last().children.addAll(richSpan)
        return
    }

    val startIndex = max(0, index - activeRichSpan.textRange.min)
    val beforeText = activeRichSpan.text.substring(0, startIndex)
    val afterText = activeRichSpan.text.substring(startIndex)

    // Simplify the richSpan tree if possible, by avoiding creating a new RichSpan.
    if (
        beforeText.isEmpty() &&
        afterText.isEmpty() &&
        activeRichSpan.children.isEmpty() &&
        richSpan.size == 1
    ) {
        activeRichSpan.text = richSpan.first().text
        activeRichSpan.richSpanStyle = richSpan.first().richSpanStyle
        return
    }

    activeRichSpan.text = beforeText

    var addedTextLength = 0
    for (i in richSpan.lastIndex downTo 0) {
        val newRichSpan = richSpan[i]
        newRichSpan.paragraph = activeRichSpan.paragraph
        newRichSpan.parent = activeRichSpan
        activeRichSpan.children.add(
            0,
            newRichSpan
        )
        addedTextLength += newRichSpan.text.length
    }
    if (afterText.isNotEmpty()) {
        activeRichSpan.children.add(
            1,
            RichSpan(
                paragraph = activeRichSpan.paragraph,
                parent = activeRichSpan,
                text = afterText,
                textRange = TextRange(
                    index + addedTextLength,
                    index + addedTextLength + afterText.length
                ),
            )
        )
    } else {
        val firstRichSpan = activeRichSpan.children.firstOrNull()
        val secondRichSpan = activeRichSpan.children.getOrNull(1)

        if (
            firstRichSpan != null &&
            secondRichSpan != null &&
            firstRichSpan.spanStyle == secondRichSpan.spanStyle
        ) {
            firstRichSpan.text += secondRichSpan.text
            firstRichSpan.children.addAll(secondRichSpan.children)
            activeRichSpan.children.removeAt(1)
        }

        if (
            firstRichSpan != null &&
            activeRichSpan.text.isEmpty() &&
            activeRichSpan.children.size == 1 &&
            richSpan.size == 1
        ) {
            activeRichSpan.text = firstRichSpan.text
            activeRichSpan.spanStyle =
                richSpan.first().spanStyle.customMerge(firstRichSpan.spanStyle)
            activeRichSpan.children.clear()
            activeRichSpan.children.addAll(firstRichSpan.children)
        }
    }
}


/**
 * Returns the [RichSpan] that contains the given [textIndex].
 * If no [RichSpan] contains the given [textIndex], null is returned.
 *
 * @param textIndex The text index to search for.
 * @return The [RichSpan] that contains the given [textIndex], or null if no such [RichSpan] exists.
 */
internal fun RichTextState.getRichSpanByTextIndex(
    textIndex: Int,
    ignoreCustomFiltering: Boolean = false,
): RichSpan? {
    // If the text index is equal or less than 0, we can return the first non-empty child of the first paragraph.
    if (textIndex < 0) {
        val firstParagraph = richParagraphList.firstOrNull() ?: return null

//            if (textIndex == 0 && firstParagraph.isEmpty() && richParagraphList.size > 1) {
//                val secondParagraph = richParagraphList[1]
//                return secondParagraph.getFirstNonEmptyChild(secondParagraph.type.startText.length)
//            }

        return firstParagraph.getFirstNonEmptyChild(firstParagraph.type.startText.length)
    }

    var index = 0
    richParagraphList.fastForEachIndexed { paragraphIndex, richParagraph ->
        val result = richParagraph.getRichSpanByTextIndex(
            paragraphIndex = paragraphIndex,
            textIndex = textIndex,
            offset = index,
            ignoreCustomFiltering = ignoreCustomFiltering,
        )
        if (result.second != null)
            return result.second
        else
            index = result.first
    }
    return null
}

/**
 * Returns a list of [RichSpan]s that contains at least a part of the given [searchTextRange].
 * If no [RichSpan] contains at least a part of the given [searchTextRange], an empty list is returned.
 *
 * @param searchTextRange The [TextRange] to search for.
 * @return A list of [RichSpan]s that contains a part of the given [searchTextRange], or an empty list if no such [RichSpan] exists.
 */
internal fun RichTextState.getRichSpanListByTextRange(searchTextRange: TextRange): List<RichSpan> {
    var index = 0
    val richSpanList = mutableListOf<RichSpan>()
    richParagraphList.fastForEachIndexed { paragraphIndex, richParagraphStyle ->
        val result = richParagraphStyle.getRichSpanListByTextRange(
            paragraphIndex = paragraphIndex,
            searchTextRange = searchTextRange,
            offset = index,
        )
        richSpanList.addAll(result.second)
        index = result.first
    }
    return richSpanList
}

internal fun RichTextState.getToShiftRichSpanList(
    startRichSpan: RichSpan,
    endRichSpan: RichSpan?,
): List<RichSpan> {
    val toShiftRichSpanList: MutableList<RichSpan> = mutableListOf()
    var previousRichSpan: RichSpan?
    var currentRichSpan: RichSpan? = startRichSpan

    while (true) {
        previousRichSpan = currentRichSpan
        currentRichSpan = currentRichSpan?.parent

        if (currentRichSpan == null || currentRichSpan == endRichSpan) {
            break
        } else {
            val index = currentRichSpan.children.indexOf(previousRichSpan)
            if (index in 0 until currentRichSpan.children.lastIndex) {
                ((index + 1)..currentRichSpan.children.lastIndex).forEach {
                    val richSpan = currentRichSpan.children[it]
                    richSpan.spanStyle = richSpan.fullSpanStyle
                    richSpan.parent = endRichSpan
                    toShiftRichSpanList.add(richSpan)
                }
                currentRichSpan.children.removeRange(index + 1, currentRichSpan.children.size)
            }
        }
    }

    return toShiftRichSpanList
}

/**
 * Apply [toAddSpanStyle] and [toRemoveSpanStyle] to a [RichSpan].
 *
 * @param richSpan The [RichSpan] to apply the styles to.
 * @param beforeText The text before applying the styles.
 * @param middleText The text to apply the styles to.
 * @param afterText The text after applying the styles.
 * @param startIndex The start index of the text to apply the styles to.
 * @param richSpanFullSpanStyle The [SpanStyle] of the [RichSpan].
 * @param newSpanStyle The new [SpanStyle] to apply to the [RichSpan].
 */
@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.handleUpdatingRichSpan(
    richSpan: RichSpan,
    beforeText: String,
    middleText: String,
    afterText: String,
    startIndex: Int,
    richSpanFullSpanStyle: SpanStyle = richSpan.fullSpanStyle,
    newSpanStyle: SpanStyle = richSpanFullSpanStyle.customMerge(toAddSpanStyle)
        .unmerge(toRemoveSpanStyle),
    newRichSpanStyle: RichSpanStyle =
        when {
            toAddRichSpanStyle != RichSpanStyle.Default -> toAddRichSpanStyle

            toRemoveRichSpanStyleKClass == richSpan.richSpanStyle::class -> RichSpanStyle.Default

            else -> richSpan.richSpanStyle
        },
) {
    if (richSpanFullSpanStyle == newSpanStyle && newRichSpanStyle::class == richSpan.richSpanStyle::class) return

    if (
        (toRemoveSpanStyle == SpanStyle() || !richSpanFullSpanStyle.isSpecifiedFieldsEquals(
            toRemoveSpanStyle
        )) &&
        (toRemoveRichSpanStyleKClass == RichSpanStyle.Default::class || newRichSpanStyle::class == richSpan.richSpanStyle::class)
    ) {
        applyStyleToRichSpan(
            richSpan = richSpan,
            beforeText = beforeText,
            middleText = middleText,
            afterText = afterText,
            startIndex = startIndex,
        )
    } else {
        handleRemovingStyleFromRichSpan(
            richSpan = richSpan,
            beforeText = beforeText,
            middleText = middleText,
            afterText = afterText,
            startIndex = startIndex,
            richSpanFullSpanStyle = richSpanFullSpanStyle,
            newSpanStyle = newSpanStyle,
            newRichSpanStyle = newRichSpanStyle,
        )
    }
}


/**
 * Handles applying a new [SpanStyle] and a new [RichSpanStyle] to a [RichSpan].
 *
 * @param richSpan The [RichSpan] to apply the new [SpanStyle] to.
 * @param beforeText The text before applying the styles.
 * @param middleText The text to apply the styles to.
 * @param afterText The text after applying the styles.
 * @param startIndex The start index of the text to apply the styles to.
 */
@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.applyStyleToRichSpan(
    richSpan: RichSpan,
    beforeText: String,
    middleText: String,
    afterText: String,
    startIndex: Int,
) {
    val fullSpanStyle = richSpan.fullSpanStyle

    // Simplify the richSpan tree if possible, by avoiding creating a new RichSpan.
    if (
        beforeText.isEmpty() &&
        afterText.isEmpty() &&
        richSpan.children.isEmpty()
    ) {
        richSpan.text = middleText
        richSpan.spanStyle = richSpan.spanStyle
            .copy(textDecoration = fullSpanStyle.textDecoration)
            .customMerge(toAddSpanStyle)
        richSpan.richSpanStyle =
            if (toAddRichSpanStyle != RichSpanStyle.Default)
                toAddRichSpanStyle
            else
                richSpan.richSpanStyle

        return
    }

    richSpan.text = beforeText
    val newRichSpan =
        RichSpan(
            paragraph = richSpan.paragraph,
            parent = richSpan,
            text = middleText,
            textRange = TextRange(
                startIndex,
                startIndex + middleText.length
            ),
            spanStyle = SpanStyle(textDecoration = fullSpanStyle.textDecoration).customMerge(
                toAddSpanStyle
            ),
            richSpanStyle =
                if (toAddRichSpanStyle != RichSpanStyle.Default)
                    toAddRichSpanStyle
                else
                    richSpan.richSpanStyle,
        )

    val parent = richSpan.parent
    val index = parent?.children?.indexOf(richSpan) ?: richSpan.paragraph.children.indexOf(richSpan)
    var isRemoved = false

    val isRichSpanStylingEmpty =
        richSpan.spanStyle == SpanStyle() && richSpan.richSpanStyle == RichSpanStyle.Default

    if (middleText.isNotEmpty()) {
        if (
            (isRichSpanStylingEmpty || richSpan.text.isEmpty()) &&
            index != -1 &&
            richSpan.children.isEmpty()
        ) {
            newRichSpan.parent = richSpan.parent

            if (!isRichSpanStylingEmpty) {
                newRichSpan.spanStyle = richSpan.spanStyle.customMerge(newRichSpan.spanStyle)
                if (richSpan.richSpanStyle != RichSpanStyle.Default && newRichSpan.richSpanStyle == RichSpanStyle.Default)
                    newRichSpan.richSpanStyle = richSpan.richSpanStyle
            }

            if (parent != null) {
                parent.children.add(index + 1, newRichSpan)

                if (richSpan.text.isEmpty()) {
                    parent.children.removeAt(index)
                    isRemoved = true
                }
            } else {
                richSpan.paragraph.children.add(index + 1, newRichSpan)

                if (richSpan.text.isEmpty()) {
                    richSpan.paragraph.children.removeAt(index)
                    isRemoved = true
                }
            }
        } else {
            richSpan.children.add(0, newRichSpan)
            newRichSpan.parent = richSpan
        }
    }

    if (afterText.isNotEmpty()) {
        val afterRichSpan =
            RichSpan(
                paragraph = richSpan.paragraph,
                parent = richSpan,
                text = afterText,
                textRange = TextRange(
                    startIndex + middleText.length,
                    startIndex + middleText.length + afterText.length
                ),
            )

        if (
            (isRichSpanStylingEmpty || richSpan.text.isEmpty()) &&
            index != -1 &&
            richSpan.children.isEmpty()
        ) {
            afterRichSpan.parent = richSpan.parent

            if (!isRichSpanStylingEmpty) {
                afterRichSpan.spanStyle =
                    richSpan.spanStyle.customMerge(afterRichSpan.spanStyle)
                if (richSpan.richSpanStyle != RichSpanStyle.Default && afterRichSpan.richSpanStyle == RichSpanStyle.Default)
                    afterRichSpan.richSpanStyle = richSpan.richSpanStyle
            }

            val addIndex =
                if (isRemoved || middleText.isEmpty())
                    index + 1
                else
                    index + 2

            if (parent != null) {
                parent.children.add(addIndex, afterRichSpan)

                if (richSpan.text.isEmpty() && !isRemoved)
                    parent.children.removeAt(index)
            } else {
                richSpan.paragraph.children.add(addIndex, afterRichSpan)

                if (richSpan.text.isEmpty() && !isRemoved)
                    richSpan.paragraph.children.removeAt(index)
            }
        } else {
            richSpan.children.add(1, afterRichSpan)
            afterRichSpan.parent = richSpan
        }
    } else {
        val firstRichSpan = richSpan.children.firstOrNull()
        val secondRichSpan = richSpan.children.getOrNull(1)

        if (
            firstRichSpan != null &&
            secondRichSpan != null &&
            firstRichSpan.spanStyle == secondRichSpan.spanStyle &&
            firstRichSpan.richSpanStyle == secondRichSpan.richSpanStyle
        ) {
            firstRichSpan.text += secondRichSpan.text
            firstRichSpan.children.addAll(secondRichSpan.children)
            richSpan.children.removeAt(1)
        }

        if (
            firstRichSpan != null &&
            richSpan.text.isEmpty() &&
            richSpan.children.size == 1
        ) {
            richSpan.text = firstRichSpan.text
            richSpan.spanStyle = richSpan.spanStyle.customMerge(firstRichSpan.spanStyle)
            richSpan.children.clear()
            richSpan.children.addAll(firstRichSpan.children)
        }
    }
}


/**
 * Handles removing a [SpanStyle] from a [RichSpan].
 *
 * @param richSpan The [RichSpan] to remove the [SpanStyle] from.
 * @param beforeText The text before removing the styles.
 * @param middleText The text to remove the styles from.
 * @param afterText The text after removing the styles.
 * @param startIndex The start index of the text to remove the styles from.
 * @param richSpanFullSpanStyle The [SpanStyle] of the [RichSpan].
 * @param newSpanStyle The new [SpanStyle] to apply to the [RichSpan].
 * @param newRichSpanStyle The new [RichSpanStyle] to apply to the [RichSpan].
 */
@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.handleRemovingStyleFromRichSpan(
    richSpan: RichSpan,
    beforeText: String,
    middleText: String,
    afterText: String,
    startIndex: Int,
    richSpanFullSpanStyle: SpanStyle,
    newSpanStyle: SpanStyle,
    newRichSpanStyle: RichSpanStyle,
) {
    richSpan.text = beforeText
    val parentRichSpan = richSpan.getClosestRichSpan(newSpanStyle, newRichSpanStyle)
    val newRichSpan = RichSpan(
        paragraph = richSpan.paragraph,
        parent = parentRichSpan,
        text = middleText,
        textRange = TextRange(
            startIndex,
            startIndex + middleText.length
        ),
        spanStyle = newSpanStyle.unmerge(parentRichSpan?.spanStyle),
        richSpanStyle = newRichSpanStyle,
    )
    val afterRichSpan = RichSpan(
        paragraph = richSpan.paragraph,
        parent = parentRichSpan,
        text = afterText,
        textRange = TextRange(
            startIndex + middleText.length,
            startIndex + middleText.length + afterText.length
        ),
        spanStyle = richSpanFullSpanStyle,
        richSpanStyle = richSpan.richSpanStyle,
    )

    val toShiftRichSpanList: MutableList<RichSpan> = mutableListOf()
    var previousRichSpan: RichSpan?
    var currentRichSpan: RichSpan? = richSpan

    toShiftRichSpanList.add(newRichSpan)
    if (afterRichSpan.text.isNotEmpty())
        toShiftRichSpanList.add(afterRichSpan)

    while (true) {
        previousRichSpan = currentRichSpan
        currentRichSpan = currentRichSpan?.parent

        if (currentRichSpan == null || currentRichSpan == parentRichSpan) {
            break
        } else {
            val index = currentRichSpan.children.indexOf(previousRichSpan)
            if (index in 0 until currentRichSpan.children.lastIndex) {
                val currentRichSpanFullSpanStyle = currentRichSpan.fullSpanStyle
                ((index + 1)..currentRichSpan.children.lastIndex).forEach {
                    val childRichSpan = currentRichSpan.children[it]

                    // Ignore shifting empty RichSpan.
                    if (childRichSpan.text.isEmpty() && childRichSpan.children.isEmpty()) {
                        return@forEach
                    }

                    // Merge RichSpan span style with parent RichSpan span style.
                    childRichSpan.spanStyle =
                        currentRichSpanFullSpanStyle.merge(childRichSpan.spanStyle)

                    // Lookup for RichSpan with the same span style and merge them to optimize the RichSpan tree.
                    val lastChild = toShiftRichSpanList.lastOrNull()
                    if (lastChild != null && lastChild.spanStyle == childRichSpan.spanStyle) {
                        if (lastChild.children.isEmpty()) {
                            lastChild.text += childRichSpan.text
                            lastChild.children.addAll(childRichSpan.children)
                        } else {
                            lastChild.children.add(childRichSpan)
                            childRichSpan.parent = lastChild
                            childRichSpan.spanStyle = RichSpanStyle.DefaultSpanStyle
                            for (i in childRichSpan.children.lastIndex downTo 0) {
                                val child = childRichSpan.children[i]
                                child.parent = lastChild
                                childRichSpan.children.removeAt(i)
                                lastChild.children.add(child)
                            }
                        }
                    } else {
                        childRichSpan.parent = parentRichSpan
                        toShiftRichSpanList.add(childRichSpan)
                    }
                }

                // Remove shifted RichSpan from parent RichSpan.
                currentRichSpan.children.removeRange(index + 1, currentRichSpan.children.size)

                // Remove empty RichSpan.
                if (previousRichSpan?.isEmpty() == true) {
                    currentRichSpan.children.removeAt(index)
                }
            }
        }
    }

    if (parentRichSpan == null || currentRichSpan == null) {
        val index = richSpan.paragraph.children.indexOf(previousRichSpan)
        if (index in 0..richSpan.paragraph.children.lastIndex) {
            richSpan.paragraph.children.addAll(
                index + 1,
                toShiftRichSpanList
            )
        }

        // Remove empty RichSpan.
        if (previousRichSpan?.isEmpty() == true) {
            richSpan.paragraph.children.removeAt(index)
        }
    } else {
        val index = parentRichSpan.children.indexOf(previousRichSpan)
        if (index in 0..parentRichSpan.children.lastIndex) {
            parentRichSpan.children.addAll(
                index + 1,
                toShiftRichSpanList
            )
        }

        // Remove empty RichSpan.
        if (previousRichSpan?.isEmpty() == true) {
            parentRichSpan.children.removeAt(index)
        }
    }

    if (richSpan.text.isEmpty() && richSpan.children.isEmpty()) {
        val parent = richSpan.parent
        if (parent != null) {
            parent.children.remove(richSpan)
        } else {
            richSpan.paragraph.children.remove(richSpan)
        }
    }
}

/**
 * Slice [RichSpan] by [startIndex] and [richSpan] that contains [startIndex].
 * The passed [RichSpan] will be modified, containing only the text before [startIndex].
 * And the new [RichSpan] will be returned, containing the text after [startIndex].
 *
 * @param startIndex The start index of the slice.
 * @param richSpan The [RichSpan] that contains [startIndex].
 * @return The new [RichSpan].
 */
@OptIn(ExperimentalRichTextApi::class)
private fun RichSpan.slice(
    startIndex: Int,
    richSpan: RichSpan,
): RichSpan {
    val newRichSpan = RichSpan(
        paragraph = richSpan.paragraph,
    )

    var previousRichSpan: RichSpan
    var currentRichSpan: RichSpan = richSpan

    val textStartIndex = startIndex - richSpan.textRange.min
    val beforeText =
        if (textStartIndex > 0) richSpan.text.substring(0, textStartIndex) else "" // + ' '
    val afterText = richSpan.text.substring(textStartIndex + 1)

    richSpan.text = beforeText
    richSpan.textRange = TextRange(
        richSpan.textRange.min,
        richSpan.textRange.min + beforeText.length
    )

    val afterRichSpan = RichSpan(
        paragraph = richSpan.paragraph,
        parent = newRichSpan,
        text = afterText,
        textRange = TextRange(
            startIndex,
            startIndex + afterText.length
        ),
        spanStyle = richSpan.fullSpanStyle,
    )

    newRichSpan.children.add(afterRichSpan)

    for (i in richSpan.children.lastIndex downTo 0) {
        val childRichSpan = richSpan.children[i]
        richSpan.children.removeAt(i)
        childRichSpan.parent = afterRichSpan
        afterRichSpan.children.add(childRichSpan)
    }

    while (true) {
        previousRichSpan = currentRichSpan
        currentRichSpan = currentRichSpan.parent ?: break

        val index = currentRichSpan.children.indexOf(previousRichSpan)
        if (index in 0 until currentRichSpan.children.lastIndex) {
            ((index + 1)..currentRichSpan.children.lastIndex).forEach {
                val childRichSpan = currentRichSpan.children[it]
                childRichSpan.spanStyle = childRichSpan.fullSpanStyle
                childRichSpan.parent = null
                newRichSpan.children.add(childRichSpan)
            }
            currentRichSpan.children.removeRange(index + 1, currentRichSpan.children.size)
        }
    }

    val index = richSpan.paragraph.children.indexOf(previousRichSpan)
    if (index in 0 until richSpan.paragraph.children.lastIndex) {
        ((index + 1)..richSpan.paragraph.children.lastIndex).forEach {
            val childRichSpan = richSpan.paragraph.children[it]
            childRichSpan.spanStyle = childRichSpan.fullSpanStyle
            childRichSpan.parent = null
            newRichSpan.children.add(childRichSpan)
        }
        richSpan.paragraph.children.removeRange(index + 1, richSpan.paragraph.children.size)
    }

    return newRichSpan
}


@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.getOrCreateRichSpanByTextIndex(
    textIndex: Int,
    ignoreCustomFiltering: Boolean = false,
): RichSpan? {
    val richSpan =
        getRichSpanByTextIndex(
            textIndex = textIndex,
            ignoreCustomFiltering = ignoreCustomFiltering,
        )

    if (richSpan == null && textIndex < 0) {
        val firstParagraph = richParagraphList.firstOrNull() ?: return null
        val newRichSpan = RichSpan(
            paragraph = firstParagraph,
            text = "",
        )
        firstParagraph.children.add(0, newRichSpan)
        return newRichSpan
    }

    return richSpan
}
